<script setup>
import {onMounted, ref} from "vue";
import {Clock, DocumentCopy} from '@element-plus/icons-vue'
import {httpPost} from "@/utils/http.js";
import {useRouter} from "vue-router";
import hl from 'highlight.js'
import {dateFormat, isImage, processPrompt} from "@/utils/libs.js";
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system.js";
// mathjax3 插件：用于在 Markdown 中解析 LaTeX 数学公式
// 通过 md.use() 加载插件后，能自动将 $...$（行内公式）和 $$...$$（独立公式）转为 MathJax 兼容的 HTML 标签。
import mathjaxPlugin from 'markdown-it-mathjax3'
import MarkDownIt from 'markdown-it'
import { full as emoji } from 'markdown-it-emoji'

const router = useRouter()

const md = new MarkDownIt({
  breaks: true, // 将换行符转换为 `<br>` 标签
  html: true, // 允许 Markdown 中嵌入 HTML 标签
  linkify: true, // 自动将 URL 文本转为超链接
  typographer: true, // 启用排版优化（如引号美化、连字符转换）
  // 代码高亮与复制功能
  // str: 表示代码块中的原始文本内容，即未经转义或高亮处理的纯代码字符串。
  // lang: 表示代码块指定的编程语言类型（如 js、python），通常由 Markdown 语法中的代码块标识符定义（如 ```js）。
  //        用于判断是否支持该语言的高亮。例如，hljs.getLanguage(lang) 会检查 highlight.js 是否内置了该语言的语法规则
  // 函数需返回包裹在 <pre><code> 标签中的高亮结果
  highlight: function (str, lang) {
    // 唯一标识生成：codeIndex 基于时间戳和随机数生成，避免复制目标 ID 冲突。
    const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
    // 显示复制代码按钮
    // 通过 Clipboard.js 的 data-clipboard-target 属性绑定隐藏的 <textarea>，实现一键复制功能
    const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
                    <textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '&lt;/textarea>')}</textarea>`
    if (lang && hl.getLanguage(lang)) {
      const langHtml = `<span class="lang-name">${lang}</span>`
      // 处理代码高亮
      const preCode = hl.highlight(lang, str, true).value
      // 将代码包裹在 pre 中
      return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
    }
    // 处理代码高亮
    const preCode = md.utils.escapeHtml(str)
    // 将代码包裹在 pre 中
    return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
  }
})
md.use(mathjaxPlugin)
md.use(emoji)

const props = defineProps({
  data: {
    type: Object,
    default: {
      content: '',
      created_at: '',
      tokens: 0,
      model: '',
      icon: '',
    }
  },
  listStyle: {
    type: String,
    default: 'list'
  }
})
// 提示词消耗的token
// const finalTokens = ref(props.data.tokens)
// processPrompt 对其进行 HTML 特殊字符转义
const content = ref(processPrompt(props.data.content))
// 提示词中的文件地址
const files = ref([])
onMounted(() => {
  processFiles()
})

// 处理提示词中的文件信息
const processFiles = () => {
  if (!props.data.content) {
    return
  }
  // 用于匹配 URL 链接的正则表达式，其作用是从文本中提取符合 http:// 或 https:// 协议格式的字符串
  // :\/\/ 匹配字面量字符串 ://。由于 / 在正则中是特殊字符，需通过反斜杠 \ 转义为普通字符
  // 例如：http:// 和 https:// 中的 :// 部分。
  // \S 表示非空白字符（等价于 [^\s]），+ 表示前面的字符出现 1 次或多次。因此 \S+ 会匹配从协议分隔符开始的所有非空字符，直到遇到空格或换行符
  // 例如：https://example.com/path?query=1 会完整匹配整个 URL。
  // g 表示全局匹配模式，允许正则表达式在文本中找到所有符合条件的子串，而非仅第一个
  // const text = "参考文档：https://a.com，示例代码：http://b.com";
  // const links = text.match(linkRegex);
  // 输出：["https://a.com", "http://b.com"]
  const linkRegex = /(https?:\/\/\S+)/g
  // 匹配内容中的链接地址，返回数组
  const links = props.data.content.match(linkRegex)
  // 获取当前页面的协议
  const urlPrefix = `${window.location.protocol}//${window.location.host}`
  if (links) {
    // 通过 links.map(...) 生成新数组 _links，原始数组 links 不会被修改
    const _links = links.map((link) => {
      // link.startsWith(urlPrefix) 判断当前链接是否以 urlPrefix 开头（如 https://example.com/），如果是，就去除 urlPrefix，如果不是直接返回
      if (link.startsWith(urlPrefix)) {
        return link.replace(urlPrefix, "")
      }
      return link
    })
    // 合并数组 并 去重
    const urls = [...new Set([...links, ..._links])]
    const isAdmin = router.currentRoute.value.fullPath.startsWith('/admin')
    httpPost(isAdmin ? 'api/admin/file/list' : 'api/file/list', {urls: urls}).then(res => {
      files.value = res.data.items
      for (let link of links) {
        // 判断 link 链接是不是图片，并且是不是外部链接，files是我们上传的文件链接可以比喻为内部链接，如果我们在写提示词的时候从别的地方复制过来一个链接放在提示词中他就是外部链接，如果这个外部链接是图片就加入到 files 中进行渲染，如果不是图片就当做文本渲染也就不用做处理，直接在content中显示连接地址
        if (isExternalImg(link, files.value)) {
          // 把 link 加入到 files 中
          files.value.push({url: link, ext: ".png"})
        }
        // files 是我们自己上传的文件链接这个链接包含在提示词中，我们在渲染内容的时候，不需要渲染我们自己上传文件的链接，因为我们直接渲染文件
        // 如果我们在提示词中写入了链接就需要把它给渲染出来
        // 我们需要判断 link 是不是我们自己上传的文件链接，如果是就把他从 content 中移除掉，如果不是就把链接渲染出来
        const isExist = files.value.some(file => file.url === link);
        if (isExist) {
          content.value = content.value.replaceAll(link, '')
        }
      }
    }).catch(() => {
    })
  }
  // md.render 将修剪后的 Markdown 文本转换为 HTML 字符串
  content.value = md.render(content.value.trim())
}

// 判断链接是不是图片，并且判断 files 中是否 包含了 link，只有链接是图片并且 files 中没有包含 link 整个方法才返回 true
const isExternalImg = (link, files) => {
  // 判断链接是不是图片 并且 判断 files 中是否包含了 link 链接，如果包含了，就返回匹配项，如果没找到就返回 undefined，在对结果进行取反
  // 如果 files 中没有包含 link 说明 link 是外部链接
  return isImage(link) && !files.find(file => file.url === link)
}

// 预览图片
const previewURL = ref('')
const previewImg = (url) => {
  previewURL.value = url
}

</script>

<template>
  <div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
    <div class="chat-line-inner">
      <!--  显示图像  -->
      <div class="chat-icon">
        <img :src="data.icon" alt="User"/>
      </div>

      <div class="chat-item">
        <!-- 显示文件 -->
        <div v-if="files.length > 0" class="file-list-box">
          <div v-for="file in files">
            <!-- 显示图片 -->
            <div class="image" v-if="isImage(file.ext)">
              <el-image :src="file.url" fit="cover" @click="previewImg(file.url)"/>
            </div>
            <!-- 如果不是图片就获取链接的扩展名，根据扩展名找文件的图标进行显示 -->
            <div class="item" v-else>
              <div class="icon">
                <!-- 显示文件图标 -->
                <el-image :src="GetFileIcon(file.ext)" fit="cover"/>
              </div>
              <div class="body">
                <div class="title">
                  <!-- 显示文件名称，点击文件名称下载文件 -->
                  <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{ file.name }}
                  </el-link>
                </div>
                <div class="info">
                  <!-- 显示文件类型和文件大小 -->
                  <span>{{ GetFileType(file.ext) }}</span>
                  <span>{{ FormatFileSize(file.size) }}</span>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- 显示文本内容 -->
        <div class="content" v-html="content"></div>
        <!-- 显示时间和token消耗 -->
        <div class="bar" v-if="data.created_at > 0">
          <span class="bar-item" style="background-color: #e7e7e8;cursor: pointer"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }} </span>
          <span class="bar-item" style="background-color: #e7e7e8;cursor: pointer">tokens：{{ data.tokens }}</span>
          <span class="bar-item" style="background-color: #e7e7e8;cursor: pointer">
            <el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
              <el-icon class="copy-prompt" :data-clipboard-text="data.content">
                <DocumentCopy/>
              </el-icon>
            </el-tooltip>
          </span>
        </div>
      </div>
    </div>
  </div>

  <div class="chat-line chat-line-prompt-chat" v-else>
    <div class="chat-line-inner">
      <!-- 显示图像  -->
      <div class="chat-icon">
        <img :src="data.icon" alt="User"/>
      </div>
      <div class="chat-item">
        <!-- 判断是否有文件显示 -->
        <div v-if="files.length > 0" class="file-list-box">
          <div v-for="file in files">
            <!-- 显示图片 -->
            <div class="image" v-if="isImage(file.ext)">
              <el-image :src="file.url" fit="cover" @click="previewImg(file.url)"/>
            </div>
            <!-- 如果不是图片就显示对应文件的信息 -->
            <div class="item" v-else>
              <!-- 显示不同类型文件的图标 -->
              <div class="icon">
                <el-image :src="GetFileIcon(file.ext)" fit="cover"/>
              </div>
              <!-- 显示文件信息 -->
              <div class="body">
                <div class="title">
                  <el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{ file.name }}
                  </el-link>
                </div>
                <div class="info">
                  <span>{{ GetFileType(file.ext) }}</span>
                  <span>{{ FormatFileSize(file.size) }}</span>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- 显示提示词内容 -->
        <div class="content-wrapper">
          <div class="content" v-html="content"></div>
        </div>
        <!-- 显示时间和消耗token -->
        <div class="bar" v-if="data.created_at > 0">
          <span class="bar-item" style="background-color: #e7e7e8;cursor: pointer"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
          <span class="bar-item" style="background-color: #e7e7e8;cursor: pointer">tokens: {{ data.tokens }}</span>
          <span class="bar-item" style="background-color: #e7e7e8;cursor: pointer">
            <el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
              <el-icon class="copy-prompt" :data-clipboard-text="data.content">
                <DocumentCopy/>
              </el-icon>
            </el-tooltip>
          </span>
        </div>
      </div>
    </div>
  </div>

  <!-- 图片预览 -->
  <el-image-viewer @close="() => {previewURL = ''}" v-if="previewURL !== ''" :url-list="[previewURL]"/>
</template>

<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
.chat-page,.chat-export {
  .chat-line-prompt-list {
    background-color: var(--chat-content-bg-list);
    color: var(--theme-text-color-primary);
    justify-content: center;
    width 100%
    padding-bottom: 1.5rem;
    padding-top: 1.5rem;
    border-bottom: 0.5px solid var(--el-border-color);

    .chat-line-inner {
      display flex;
      width 100%;
      max-width 900px;
      padding-left 10px;

      .chat-icon {
        margin-right 20px;

        img {
          width: 36px;
          height: 36px;
          border-radius: 50%;
          padding: 1px;
        }
      }

      .chat-item {
        width 100%
        padding: 0 5px 0 0;
        overflow: hidden;

        .file-list-box {
          display flex
          flex-flow column
          .image {
            display flex
            flex-flow row
            margin-right 10px
            position relative

            .el-image {
              border 1px solid #e3e3e3
              border-radius 10px
              margin-bottom 10px
              width 300px
              height 300px
            }
          }
          .item {
            display flex
            flex-flow row
            border-radius 10px
            background-color:var(--chat-content-bg);
            border 1px solid #e3e3e3
            color:var(--theme-text-color-primary);
            padding 6px
            margin-bottom 10px

            .icon {
              .el-image {
                width 40px
                height 40px
              }
            }
            .body {
              margin-left 8px
              font-size 14px
              .title {
                font-weight bold
                line-height 24px
                color #0D0D0D
              }
              .info {
                color #B4B4B4

                span {
                  margin-right 10px
                }
              }
            }
          }
        }

        .content {
          word-break break-word;
          padding: 0;
          color:var(--theme-text-color-primary)
          font-size: var(--content-font-size);
          border-radius: 5px;
          overflow: auto;

          img {
            max-width: 600px;
            border-radius: 10px;
            margin 10px 0
          }

          p {
            line-height 1.5
          }

          p:last-child {
            margin-bottom: 0
          }

          p:first-child {
            margin-top 0
          }
        }

        .bar {
          padding 10px 10px 10px 0;

          .bar-item {
            /*background-color #f7f7f8;*/
            color #888
            padding 3px 5px;
            margin-right 10px;
            border-radius 5px;

            .el-icon {
              position relative
              top 2px;
            }
          }
        }
      }
    }


  }

  .chat-line-prompt-chat {
    background: var(--chat-bg);
    justify-content: center;
    width 100%
    padding-bottom: 1.5rem;
    padding-top: 1.5rem;

    .chat-line-inner {
      display flex;
      width 100%;
      padding 0 25px;
      flex-flow row-reverse

      .chat-icon {
        margin-left 20px;

        img {
          width: 36px;
          height: 36px;
          border-radius: 50%;
          padding: 1px;
        }
      }

      .chat-item {
        padding: 0;
        overflow: hidden;
        max-width 60%

        .file-list-box {
          display flex
          flex-flow column
          .image {
            display flex
            flex-flow row
            margin-right 10px
            position relative

            .el-image {
              border 1px solid #e3e3e3
              border-radius 10px
              margin-bottom 10px
              width 300px
              height 300px
            }
          }
          .item {
            display flex
            flex-flow row
            border-radius 10px
            background-color:var(--chat-content-bg);
            color:var(--theme-text-color-primary);
            border 1px solid #e3e3e3
            padding 6px
            margin-bottom 10px

            .icon {
              .el-image {
                width 40px
                height 40px
              }
            }
            .body {
              margin-left 8px
              font-size 14px
              .title {
                font-weight bold
                line-height 24px
                color #0D0D0D
              }
              .info {
                color #B4B4B4

                span {
                  margin-right 10px
                }
              }
            }
          }
        }


        .content-wrapper {
          display flex
          flex-flow row-reverse
          .content {
            word-break break-word;
            padding: 1rem
            color var(--theme-text-primary);
            font-size: var(--content-font-size);
            overflow: auto;
            background-color: var(--chat-content-bg);
            border-radius: 10px 0 10px 10px;

            img {
              max-width: 600px;
              border-radius: 10px;
              margin 10px 0
            }

            p {
              line-height 1.5
            }

            p:last-child {
              margin-bottom: 0
            }

            p:first-child {
              margin-top 0
            }
          }

        }
        .bar {
          padding 10px 10px 10px 0;

          .bar-item {
            color #888
            padding 3px 5px;
            margin-right 10px;
            border-radius 5px;

            .el-icon {
              position relative
              top 2px;
            }
          }
        }
      }
    }


  }

}
</style>